﻿using JESAI.DistributedLock.Internal.Data;
using Oracle.ManagedDataAccess.Client;
using System;
using System.Data;
using System.Threading;
using System.Threading.Tasks;

namespace JESAI.DistributedLock.Oracle;

internal class OracleDatabaseConnection : DatabaseConnection
{
    public const string ApplicationNameIndicatorPrefix = "__DistributedLock.ApplicationName=";

    //请参阅SleepAsync（）以了解我们为什么需要它
    private readonly IDbConnection _innerConnection;

    public OracleDatabaseConnection(IDbConnection connection)
        : this(connection, isExternallyOwned: true)
    {
    }

    public OracleDatabaseConnection(IDbTransaction transaction)
        : base(transaction, isExternallyOwned: true)
    {
        this._innerConnection = transaction.Connection;
    }

    public OracleDatabaseConnection(string connectionString)
        : this(CreateConnection(connectionString), isExternallyOwned: false)
    {
    }

    private OracleDatabaseConnection(IDbConnection connection, bool isExternallyOwned)
        : base(connection, isExternallyOwned)
    {
        this._innerConnection = connection;
    }

    // 从https://docs.oracle.com/html/E10927_01/OracleCommandClass.htm “this method is a no-op” 使用 “Prepare()”
    public override bool ShouldPrepareCommands => false;

    public override bool IsCommandCancellationException(Exception exception) =>
        exception is OracleException oracleException
            //基于 https://docs.oracle.com/cd/E85694_01/ODPNT/CommandCancel.htm
            && (oracleException.Number == 01013 || oracleException.Number == 00936 || oracleException.Number == 00604);

    public override async Task SleepAsync(TimeSpan sleepTime, CancellationToken cancellationToken, Func<DatabaseCommand, CancellationToken, ValueTask<int>> executor)
    {
        using var sleepCommand = this.CreateCommand();
        sleepCommand.SetCommandText("BEGIN sys.DBMS_SESSION.SLEEP(:seconds) END;");
        sleepCommand.AddParameter("seconds", sleepTime.TotalSeconds);

        try
        {
            await executor(sleepCommand, cancellationToken).ConfigureAwait(false);
        }
        catch when (!cancellationToken.IsCancellationRequested)
        {
            // 除非观察到状态或明确打开/关闭连接，否则Oracle不会触发StateChange。因此，我们在看到任何异常时都会观察状态，以便事件发生。请参阅https://github.com/oracle/dotnet-db-samples/issues/226
            _ = this._innerConnection.State;
            throw;
        }
    }

    public static OracleConnection CreateConnection(string connectionString)
    {
        if (connectionString == null) { throw new ArgumentNullException(connectionString, nameof(connectionString)); }

        //那个。NET Oracle提供程序当前不支持ApplicationName作为连接字符串属性。
        //然而，我们的许多测试都依赖于该功能。作为一种解决方法，我们允许使用自定义编码方案将应用程序名称包含在连接字符串中。这仅适用于测试！
        //请参阅https://github.com/oracle/dotnet-db-samples/issues/216更多上下文。
        if (connectionString.StartsWith(ApplicationNameIndicatorPrefix, StringComparison.Ordinal))
        {
            var firstSeparatorIndex = connectionString.IndexOf(';');
            var applicationName = connectionString.Substring(startIndex: ApplicationNameIndicatorPrefix.Length, length: firstSeparatorIndex - ApplicationNameIndicatorPrefix.Length);
            // 将Oracle客户端升级到23.6.1后，连接池有时似乎会超出严格要求。
            // 如果我们按名称跟踪连接，则会导致问题。因此，我们禁用命名连接上的池化
            var connection = new OracleConnection(connectionString.Substring(startIndex: firstSeparatorIndex + 1));
            connection.ConnectionOpen += _ => connection.ClientInfo = applicationName;
            return connection;
        }

        return new OracleConnection(connectionString);
    }
}
